iT邦幫忙

0

React源碼 commit階段詳解

nero 2021-02-19 11:18:091558 瀏覽
  • 分享至 

  • xImage
  •  

點擊進入React源碼調試倉庫。

當render階段完成後,意味著在內存中構建的workInProgress樹所有更新工作已經完成,這包括樹中fiber節點的更新、diff、effectTag的標記、effectList的收集。此時workInProgress樹的完整形態如下:

current樹和workInProgress樹

和current樹相比,它們的結構上固然存在區別,變化的fiber節點也存在於workInProgress樹,但要將這些節點應用到DOM上卻不會循環整棵樹,而是通過循環effectList這個鏈表來實現,這樣保證了只針對有變化的節點做工作。

所以循環effectList鏈表去將有更新的fiber節點應用到頁面上是commit階段的主要工作。

commit階段的入口是commitRoot函數,它會告知scheduler以立即執行的優先級去調度commit階段的工作。

function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

scheduler去調度的是commitRootImpl,它是commit階段的核心實現,整個commit階段被劃分成三個部分。

commit流程概覽

commit階段主要是針對root上收集的effectList進行處理。在真正的工作開始之前,有壹個準備階段,主要是變量的賦值,以及將root的effect加入到effectList中。隨後開始針對effectList分三個階段進行工作:

  • before mutation:讀取組件變更前的狀態,針對類組件,調用getSnapshotBeforeUpdate,讓我們可以在DOM變更前獲取組件實例的信息;針對函數組件,異步調度useEffect。
  • mutation:針對HostComponent,進行相應的DOM操作;針對類組件,調用componentWillUnmount;針對函數組件,執行useLayoutEffect的銷毀函數。
  • layout:在DOM操作完成後,讀取組件的狀態,針對類組件,調用生命周期componentDidMount和componentDidUpdate,調用setState的回調;針對函數組件填充useEffect 的 effect執行數組,並調度useEffect

before mutation和layout針對函數組件的useEffect調度是互斥的,只能發起壹次調度

workInProgress 樹切換到current樹的時機是在mutation結束後,layout開始前。這樣做的原因是在mutation階段調用類組件的componentWillUnmount的時候,
還可以獲取到卸載前的組件信息;在layout階段調用componentDidMount/Update時,獲取的組件信息更新後的。

function commitRootImpl(root, renderPriorityLevel) {

  // 進入commit階段,先執行壹次之前未執行的useEffect
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  // 準備階段-----------------------------------------------

  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  if (finishedWork === null) {
    return null;
  }
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  root.callbackNode = null;
  root.callbackId = NoLanes;

  // effectList的整理,將root上的effect連到effectList的末尾
  let firstEffect;
  if (finishedWork.effectTag > PerformedWork) {
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // There is no effect on the root.
    firstEffect = finishedWork.firstEffect;
  }

  // 準備階段結束,開始處理effectList
  if (firstEffect !== null) {

    ...

    // before mutation階段--------------------------------
    nextEffect = firstEffect;
    do {...} while (nextEffect !== null);

    ...

    // mutation階段---------------------------------------
    nextEffect = firstEffect;
    do {...} while (nextEffect !== null);

    // 將wprkInProgress樹切換為current樹
    root.current = finishedWork;

    // layout階段-----------------------------------------
    nextEffect = firstEffect;
    do {...} while (nextEffect !== null);

    nextEffect = null;

    // 通知瀏覽器去繪制
    requestPaint();

  } else {
    // 沒有effectList,直接將wprkInProgress樹切換為current樹
    root.current = finishedWork;

  }

  const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

  // 獲取尚未處理的優先級,比如之前被跳過的任務的優先級
  remainingLanes = root.pendingLanes;
  // 將被跳過的優先級放到root上的pendingLanes(待處理的優先級)上
  markRootFinished(root, remainingLanes);

  /*
  * 每次commit階段完成後,再執行壹遍ensureRootIsScheduled,確保是否還有任務需要被調度。
  * 例如,高優先級插隊的更新完成後,commit完成後,還會再執行壹遍,保證之前跳過的低優先級任務
  * 重新調度
  *
  * */
  ensureRootIsScheduled(root, now());

  ...

  return null;
}

下面的部分,是對這三個階段分別進行的詳細講解。

before Mutation

beforeMutation階段的入口函數是commitBeforeMutationEffects

nextEffect = firstEffect;
do {
  try {
    commitBeforeMutationEffects();
  } catch (error) {
    ...
  }
} while (nextEffect !== null);

它的作用主要是調用類組件的getSnapshotBeforeUpdate,針對函數組件,異步調度useEffect。

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    ...

    const flags = nextEffect.flags;
    if ((flags & Snapshot) !== NoFlags) {
      // 通過commitBeforeMutationEffectOnFiber調用getSnapshotBeforeUpdate
      commitBeforeMutationEffectOnFiber(current, nextEffect);
    }

    if ((flags & Passive) !== NoFlags) {
      // 異步調度useEffect
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        scheduleCallback(NormalSchedulerPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}

commitBeforeMutationEffectOnFiber代碼如下

function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
): void {
  switch (finishedWork.tag) {
    ...
    case ClassComponent: {
      if (finishedWork.flags & Snapshot) {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 調用getSnapshotBeforeUpdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          // 將返回值存儲在內部屬性上,方便componentDidUpdate獲取
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      return;
    }
    ...
  }

}

mutation

mutation階段會真正操作DOM節點,涉及到的操作有增、刪、改。入口函數是commitMutationEffects

    nextEffect = firstEffect;
    do {
      try {
        commitMutationEffects(root, renderPriorityLevel);
      } catch (error) {
        ...
        nextEffect = nextEffect.nextEffect;
      }
    } while (nextEffect !== null);

由於過程較為復雜,所以我寫了三篇文章來說明這三種DOM操作,如果想要了解細節,可以看壹下。文章寫於17還未正式發布的時候,所以裏面的源碼版本取自17.0.0-alpha0。

React和DOM的那些事-節點新增算法

React和DOM的那些事-節點刪除算法

React和DOM的那些事-節點更新

layout階段

layout階段的入口函數是commitLayoutEffects

nextEffect = firstEffect;
do {
  try {
    commitLayoutEffects(root, lanes);
  } catch (error) {
    ...
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);

我們只關註classComponent和functionComponent。針對前者,調用生命周期componentDidMount和componentDidUpdate,調用setState的回調;針對後者,填充useEffect 的 effect執行數組,並調度useEffect(具體的原理在我的這篇文章:梳理useEffect和useLayoutEffect的原理與區別中有講解)。

function commitLifeCycles(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      // 執行useLayoutEffect的創建
      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

      // 填充useEffect的effect執行數組
      schedulePassiveEffects(finishedWork);
      return;
    }
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.flags & Update) {
        if (current === null) {
          // 如果是初始掛載階段,調用componentDidMount
          instance.componentDidMount();
        } else {
          // 如果是更新階段,調用componentDidUpdate
          const prevProps =
            finishedWork.elementType === finishedWork.type
              ? current.memoizedProps
              : resolveDefaultProps(finishedWork.type, current.memoizedProps);
          const prevState = current.memoizedState;

          instance.componentDidUpdate(
            prevProps,
            prevState,
            // 將getSnapshotBeforeUpdate的結果傳入
            instance.__reactInternalSnapshotBeforeUpdate,
          );
        }
      }

      // 調用setState的回調
      const updateQueue: UpdateQueue<
        *,
      > | null = (finishedWork.updateQueue: any);
      if (updateQueue !== null) {

        commitUpdateQueue(finishedWork, updateQueue, instance);
      }
      return;
    }

    ...

  }
}

總結

commit階段將effectList的處理分成三個階段保證了不同生命周期函數的適時調用。相對於同步執行的useEffectLayout,useEffect的異步調度提供了壹種不阻塞頁面渲染的副作用操作入口。另外,標記root上還未處理的優先級和調用ensureRootIsScheduled使得被跳過的低優先級任務得以再次被調度。commit階段的完成,也就意味著本次更新已經結束。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言